export interface ICreateArgs {
	[key: string]: string | { [key: string]: string } | undefined;
	attributes?: { [key: string]: string };
}

const has_dom_contextual_fragment = Boolean(typeof Range !== 'undefined' && Range && Range.prototype.createContextualFragment);

/**
 * Indicates whether the given parent contains the given node.
 * @param parent The parent node to check within
 * @param node The node to test whether parent is its ancestor
 * @return `true` if parent contains node, `false` otherwise
 */
export function contains(parent: Element, node: Node): boolean {
	// While modern browsers do support parent.contains, some support it only on HTML elements,
	// and IE has a known bug involving passing a text node as the argument:
	// https:// connect.microsoft.com/IE/feedback/details/780874/node-contains-is-incorrect
	// Meanwhile, compareDocumentPosition works in all supported browsers.

	if (node == null) {
		return false;
	}
	if (parent === node) {
		return true;
	}
	return Boolean(node.compareDocumentPosition(parent) & Node.DOCUMENT_POSITION_CONTAINS);
}

/*
 * Creates an Element.
 *
 * @param tagName Type of Element to create
 * @param kwArgs An object containing properties to add to the Element.
 *               If an "attributes" property is present, each member of this sub-object will be added to the Element
 *               via element.setAttribute
 * @param children An array of Nodes or strings, the latter of which will be converted to Text nodes
 * @return The created Element, with any passed properties/attributes applied
 *
 * @example
 * var div = dom.create('div', { className: 'loaded', attributes: { 'data-index': '1' } });
 *
 * @example
 * var div = dom.create('ul', null, [ dom.create('li'), dom.create('li') ]);
 *
 * @example
 * var div = dom.create('div', null, [ 'hello', ' ', 'world' ]);
 */

// These signatures are largely based on dom.generated.d.ts, minus obsoleteexport function create tags.
export function create(tagName: 'a', kwArgs?: ICreateArgs, children?: Array<Node | string>): HTMLAnchorElement;
// export function create(tagName: 'abbr', kwArgs?: CreateArgs, children?: (Node | string)[]): HTMLPhraseElement;
// export function create(tagName: 'acronym', kwArgs?: CreateArgs, children?: (Node | string)[]): HTMLPhraseElement;
// export function create(tagName: 'address', kwArgs?: CreateArgs, children?: (Node | string)[]): HTMLBlockElement;
export function create(tagName: 'area', kwArgs?: ICreateArgs, children?: Array<Node | string>): HTMLAreaElement;
export function create(tagName: 'audio', kwArgs?: ICreateArgs, children?: Array<Node | string>): HTMLAudioElement;
// export function create(tagName: 'b', kwArgs?: CreateArgs, children?: (Node | string)[]): HTMLPhraseElement;
export function create(tagName: 'base', kwArgs?: ICreateArgs, children?: Array<Node | string>): HTMLBaseElement;
// export function create(tagName: 'bdo', kwArgs?: CreateArgs, children?: (Node | string)[]): HTMLPhraseElement;
// export function create(tagName: 'big', kwArgs?: CreateArgs, children?: (Node | string)[]): HTMLPhraseElement;
// export function create(tagName: 'blockquote', kwArgs?: CreateArgs, children?: (Node | string)[]): HTMLBlockElement;
export function create(tagName: 'body', kwArgs?: ICreateArgs, children?: Array<Node | string>): HTMLBodyElement;
export function create(tagName: 'br', kwArgs?: ICreateArgs, children?: Array<Node | string>): HTMLBRElement;
export function create(tagName: 'button', kwArgs?: ICreateArgs, children?: Array<Node | string>): HTMLButtonElement;
export function create(tagName: 'canvas', kwArgs?: ICreateArgs, children?: Array<Node | string>): HTMLCanvasElement;
export function create(tagName: 'caption', kwArgs?: ICreateArgs, children?: Array<Node | string>): HTMLTableCaptionElement;
// export function create(tagName: 'center', kwArgs?: CreateArgs, children?: (Node | string)[]): HTMLBlockElement;
// export function create(tagName: 'cite', kwArgs?: CreateArgs, children?: (Node | string)[]): HTMLPhraseElement;
// export function create(tagName: 'code', kwArgs?: CreateArgs, children?: (Node | string)[]): HTMLPhraseElement;
export function create(tagName: 'col', kwArgs?: ICreateArgs, children?: Array<Node | string>): HTMLTableColElement;
export function create(tagName: 'colgroup', kwArgs?: ICreateArgs, children?: Array<Node | string>): HTMLTableColElement;
export function create(tagName: 'datalist', kwArgs?: ICreateArgs, children?: Array<Node | string>): HTMLDataListElement;
// export function create(tagName: 'dd', kwArgs?: CreateArgs, children?: (Node | string)[]): HTMLDDElement;
export function create(tagName: 'del', kwArgs?: ICreateArgs, children?: Array<Node | string>): HTMLModElement;
// export function create(tagName: 'dfn', kwArgs?: CreateArgs, children?: (Node | string)[]): HTMLPhraseElement;
export function create(tagName: 'div', kwArgs?: ICreateArgs, children?: Array<Node | string>): HTMLDivElement;
export function create(tagName: 'dl', kwArgs?: ICreateArgs, children?: Array<Node | string>): HTMLDListElement;
// export function create(tagName: 'dt', kwArgs?: CreateArgs, children?: (Node | string)[]): HTMLDTElement;
// export function create(tagName: 'em', kwArgs?: CreateArgs, children?: (Node | string)[]): HTMLPhraseElement;
export function create(tagName: 'embed', kwArgs?: ICreateArgs, children?: Array<Node | string>): HTMLEmbedElement;
export function create(tagName: 'fieldset', kwArgs?: ICreateArgs, children?: Array<Node | string>): HTMLFieldSetElement;
export function create(tagName: 'font', kwArgs?: ICreateArgs, children?: Array<Node | string>): HTMLFontElement;
export function create(tagName: 'form', kwArgs?: ICreateArgs, children?: Array<Node | string>): HTMLFormElement;
export function create(tagName: 'h1', kwArgs?: ICreateArgs, children?: Array<Node | string>): HTMLHeadingElement;
export function create(tagName: 'h2', kwArgs?: ICreateArgs, children?: Array<Node | string>): HTMLHeadingElement;
export function create(tagName: 'h3', kwArgs?: ICreateArgs, children?: Array<Node | string>): HTMLHeadingElement;
export function create(tagName: 'h4', kwArgs?: ICreateArgs, children?: Array<Node | string>): HTMLHeadingElement;
export function create(tagName: 'h5', kwArgs?: ICreateArgs, children?: Array<Node | string>): HTMLHeadingElement;
export function create(tagName: 'h6', kwArgs?: ICreateArgs, children?: Array<Node | string>): HTMLHeadingElement;
export function create(tagName: 'head', kwArgs?: ICreateArgs, children?: Array<Node | string>): HTMLHeadElement;
export function create(tagName: 'hr', kwArgs?: ICreateArgs, children?: Array<Node | string>): HTMLHRElement;
// export function create(tagName: 'i', kwArgs?: CreateArgs, children?: (Node | string)[]): HTMLPhraseElement;
export function create(tagName: 'iframe', kwArgs?: ICreateArgs, children?: Array<Node | string>): HTMLIFrameElement;
export function create(tagName: 'img', kwArgs?: ICreateArgs, children?: Array<Node | string>): HTMLImageElement;
export function create(tagName: 'input', kwArgs?: ICreateArgs, children?: Array<Node | string>): HTMLInputElement;
export function create(tagName: 'ins', kwArgs?: ICreateArgs, children?: Array<Node | string>): HTMLModElement;
// export function create(tagName: 'isindex', kwArgs?: CreateArgs, children?: (Node | string)[]): HTMLIsIndexElement;
// export function create(tagName: 'kbd', kwArgs?: CreateArgs, children?: (Node | string)[]): HTMLPhraseElement;
// export function create(tagName: 'keygen', kwArgs?: CreateArgs, children?: (Node | string)[]): HTMLBlockElement;
export function create(tagName: 'label', kwArgs?: ICreateArgs, children?: Array<Node | string>): HTMLLabelElement;
export function create(tagName: 'legend', kwArgs?: ICreateArgs, children?: Array<Node | string>): HTMLLegendElement;
export function create(tagName: 'li', kwArgs?: ICreateArgs, children?: Array<Node | string>): HTMLLIElement;
export function create(tagName: 'link', kwArgs?: ICreateArgs, children?: Array<Node | string>): HTMLLinkElement;
export function create(tagName: 'map', kwArgs?: ICreateArgs, children?: Array<Node | string>): HTMLMapElement;
export function create(tagName: 'menu', kwArgs?: ICreateArgs, children?: Array<Node | string>): HTMLMenuElement;
export function create(tagName: 'meta', kwArgs?: ICreateArgs, children?: Array<Node | string>): HTMLMetaElement;
// export function create(tagName: 'nobr', kwArgs?: CreateArgs, children?: (Node | string)[]): HTMLPhraseElement;
export function create(tagName: 'object', kwArgs?: ICreateArgs, children?: Array<Node | string>): HTMLObjectElement;
export function create(tagName: 'ol', kwArgs?: ICreateArgs, children?: Array<Node | string>): HTMLOListElement;
export function create(tagName: 'optgroup', kwArgs?: ICreateArgs, children?: Array<Node | string>): HTMLOptGroupElement;
export function create(tagName: 'option', kwArgs?: ICreateArgs, children?: Array<Node | string>): HTMLOptionElement;
export function create(tagName: 'p', kwArgs?: ICreateArgs, children?: Array<Node | string>): HTMLParagraphElement;
export function create(tagName: 'param', kwArgs?: ICreateArgs, children?: Array<Node | string>): HTMLParamElement;
export function create(tagName: 'pre', kwArgs?: ICreateArgs, children?: Array<Node | string>): HTMLPreElement;
export function create(tagName: 'progress', kwArgs?: ICreateArgs, children?: Array<Node | string>): HTMLProgressElement;
export function create(tagName: 'q', kwArgs?: ICreateArgs, children?: Array<Node | string>): HTMLQuoteElement;
// (tagName: 'rt', kwArgs?: CreateArgs, children?: (Node | string)[]): HTMLPhraseElement;
// (tagName: 'ruby', kwArgs?: CreateArgs, children?: (Node | string)[]): HTMLPhraseElement;
// (tagName: 's', kwArgs?: CreateArgs, children?: (Node | string)[]): HTMLPhraseElement;
// (tagName: 'samp', kwArgs?: CreateArgs, children?: (Node | string)[]): HTMLPhraseElement;
export function create(tagName: 'script', kwArgs?: ICreateArgs, children?: Array<Node | string>): HTMLScriptElement;
export function create(tagName: 'select', kwArgs?: ICreateArgs, children?: Array<Node | string>): HTMLSelectElement;
// (tagName: 'small', kwArgs?: CreateArgs, children?: (Node | string)[]): HTMLPhraseElement;
export function create(tagName: 'source', kwArgs?: ICreateArgs, children?: Array<Node | string>): HTMLSourceElement;
export function create(tagName: 'span', kwArgs?: ICreateArgs, children?: Array<Node | string>): HTMLSpanElement;
// (tagName: 'strong', kwArgs?: CreateArgs, children?: (Node | string)[]): HTMLPhraseElement;
export function create(tagName: 'style', kwArgs?: ICreateArgs, children?: Array<Node | string>): HTMLStyleElement;
// (tagName: 'sub', kwArgs?: CreateArgs, children?: (Node | string)[]): HTMLPhraseElement;
// (tagName: 'sup', kwArgs?: CreateArgs, children?: (Node | string)[]): HTMLPhraseElement;
export function create(tagName: 'table', kwArgs?: ICreateArgs, children?: Array<Node | string>): HTMLTableElement;
export function create(tagName: 'tbody', kwArgs?: ICreateArgs, children?: Array<Node | string>): HTMLTableSectionElement;
export function create(tagName: 'td', kwArgs?: ICreateArgs, children?: Array<Node | string>): HTMLTableDataCellElement;
export function create(tagName: 'textarea', kwArgs?: ICreateArgs, children?: Array<Node | string>): HTMLTextAreaElement;
export function create(tagName: 'tfoot', kwArgs?: ICreateArgs, children?: Array<Node | string>): HTMLTableSectionElement;
export function create(tagName: 'th', kwArgs?: ICreateArgs, children?: Array<Node | string>): HTMLTableHeaderCellElement;
export function create(tagName: 'thead', kwArgs?: ICreateArgs, children?: Array<Node | string>): HTMLTableSectionElement;
export function create(tagName: 'title', kwArgs?: ICreateArgs, children?: Array<Node | string>): HTMLTitleElement;
export function create(tagName: 'tr', kwArgs?: ICreateArgs, children?: Array<Node | string>): HTMLTableRowElement;
export function create(tagName: 'track', kwArgs?: ICreateArgs, children?: Array<Node | string>): HTMLTrackElement;
// (tagName: 'tt', kwArgs?: CreateArgs, children?: (Node | string)[]): HTMLPhraseElement;
// (tagName: 'u', kwArgs?: CreateArgs, children?: (Node | string)[]): HTMLPhraseElement;
export function create(tagName: 'ul', kwArgs?: ICreateArgs, children?: Array<Node | string>): HTMLUListElement;
// (tagName: 'var', kwArgs?: CreateArgs, children?: (Node | string)[]): HTMLPhraseElement;
export function create(tagName: 'video', kwArgs?: ICreateArgs, children?: Array<Node | string>): HTMLVideoElement;
export function create(tagName: string, kwArgs?: ICreateArgs, children?: Array<Node | string>): HTMLElement;
export function create(
	tagName: string, kwArgs?: ICreateArgs, children?: Array<Node | string>) {
	const element = document.createElement(tagName);
	if (children) {
		for (let child of children) {
			if (typeof child === 'string') {
				child = document.createTextNode(child as string);
			}
			element.appendChild(child as Node);
		}
	}
	if (kwArgs) {
		for (const property in kwArgs) {
			if (property === 'attributes') {
				if (kwArgs.attributes) {
					for (const attribute in kwArgs.attributes) {
						element.setAttribute(attribute, kwArgs.attributes[attribute]);
					}
				}
			} else {
				(element as any)[property] = kwArgs[property];
			}
		}
	}
	return element as any;
}

// Tag trees for element creation, used by fromString
const tagWrap: { [key: string]: string[] & { pre?: string; post?: string; }; } = {
	caption: ['table'],
	col: ['table', 'colgroup'],
	colgroup: ['table'],
	optgroup: ['select'],
	option: ['select'],
	rp: ['ruby'],
	rt: ['ruby'],
	rtc: ['ruby'],
	source: ['audio'],
	tbody: ['table'],
	td: ['table', 'tbody', 'tr'],
	tfoot: ['table'],
	th: ['table', 'thead', 'tr'],
	thead: ['table'],
	tr: ['table', 'tbody']
};

for (const param in tagWrap) {
	const tw = tagWrap[param];
	tw.pre = param === 'option' ? '<select multiple="multiple">' : '<' + tw.join('><') + '>';
	tw.post = '</' + tw.reverse().join('></') + '>';
}

/**
 * Creates a DocumentFragment from a string.
 *
 * @param html string representation of nodes to create
 * @return DocumentFragment containing childNodes based on html string
 *
 * @example
 * var fragment = dom.fromString('<div></div>');
 *
 * @example
 * var fragment = dom.fromString('<div></div><span></span>');
 *
 * @example
 * var fragment = dom.fromString('<tr>');
 */
export function fromString(html: string): DocumentFragment {
	let fragment: DocumentFragment;

	let master = document.createElement('div');
	const match = String(html).match(/<\s*([\w\:]+)/);
	const tag = match ? match[1].toLowerCase() : '';

	function unwrapElement(element: DocumentFragment | HTMLElement, levels: number) {
		for (let i = 0; i < levels; i++) {
			element = element.firstChild as HTMLElement;
		}

		return element;
	}

	if (has_dom_contextual_fragment) {
		master.style.display = 'none';
		document.body.appendChild(master);
		const range = document.createRange();
		range.selectNode(master);

		if (match && tagWrap[tag]) {
			const wrap = tagWrap[tag];
			const wrappedHTML = wrap.pre + html + wrap.post;
			fragment = range.createContextualFragment(wrappedHTML);
			fragment = unwrapElement(fragment, wrap.length) as DocumentFragment;
		} else {
			fragment = range.createContextualFragment(html);
		}
	} else {
		if (match && tagWrap[tag]) {
			const wrap = tagWrap[tag];
			const wrappedHTML = wrap.pre + html + wrap.post;
			master.innerHTML = wrappedHTML;
			master = unwrapElement(master, wrap.length) as HTMLDivElement;
		} else {
			master.innerHTML = html;
		}

		fragment = document.createDocumentFragment();
		let firstChild: Node | null;
		// tslint:disable-next-line:no-conditional-assignment
		while (firstChild = master.firstChild) {
			fragment.appendChild(firstChild);
		}
	}

	return fragment;
}

/*
 * Positions used with the place API for node placement.
 */
export enum Position {
	After,
	Before,
	FirstIn,
	LastIn,
	Replace
}

/**
 * Places a node in the DOM relative to another node.
 *
 * @param node The node to place in the DOM
 * @param position The position to place the node, relative to relativeElement
 * @param relativeElement The node to use as a reference when placing
 *
 * @example
 * dom.place(node, dom.Position.After, anotherNode);
 */
export function place(node: Node, position: Position, relativeElement: Element): void {
	let parent: Node | null;

	if (position === Position.After || position === Position.Before || position === Position.Replace) {
		parent = relativeElement.parentNode;
		if (!parent) {
			throw new ReferenceError('dom.place: Reference node must have a parent to determine placement');
		}

		if (position === Position.After) {
			if (parent.lastChild === relativeElement) {
				parent.appendChild(node);
			} else {
				parent.insertBefore(node, relativeElement.nextSibling);
			}
		} else if (position === Position.Before) {
			parent.insertBefore(node, relativeElement);
		} else if (position === Position.Replace) {
			parent.replaceChild(node, relativeElement);
		}
	} else if (position === Position.FirstIn) {
		relativeElement.insertBefore(node, relativeElement.firstChild);
	} else {
		// LastIn
		relativeElement.appendChild(node);
	}
}

/**
 * Removes a node from the DOM.
 *
 * @param node The node to remove
 */
export function remove(node: Node) {
	if (node.parentNode) {
		node.parentNode.removeChild(node);
	}
}

export function destroy(node: HTMLElement, parent?: HTMLElement) {
	if (!parent) {
		parent = node.parentElement;
	}
	if (parent) {
		parent.removeChild(node);
	}
}
